Elixir 繼承 Erlang 語言的特性,說平行運算是它最重要的功能也不為過。先前曾提到在 Elixir / Erlang 裡的 light-weight process,啟動一個約 1μs,運作起來像是 OO 語言裡的 instance,今天我們就來試試看。
在 Elixir 裡,啟動一個 process 非常簡單:
iex> spawn fn -> 1 + 2 end
#PID<0.92.0>
我們開了一個 process 請他計算 1 + 2
。不過由於我們沒有跟它說算完之後要怎麼辦。所以它算完就把值丟掉,並且默默的離開了。
為了要有效的溝通,每個 process 都有一個名字,叫做 pid
,要拿到每個 process 自己的 pid
,要用 self/0
。而 iex 本身,也是一個 process:
iex> self
#PID<0.88.0> # 由於系統運作的關係,你的 pid 應該不會是同一個數值
我們想讓新的 process 將計算完的結果回傳,就是給它我們的 pid
,並請它把計算後的結果傳給這個 pid
:
iex> pid = self
iex> spawn fn -> send(pid, 1 + 2) end
#PID<0.100.0>
雖然看起來還是什麼事都沒有發生,但其實結果已經寄到我們的信箱裡了。我們要用 receive/1
來接收這個訊息:
iex> receive do
...> i -> IO.inspect i
...> end
3
spawn/3
、send/2
、receive/1
是 Elixir / Erlang 裡平行運算的最基礎元素。根基於其上,Erlang 發展出一整套平行運算的理論與模式,並實作了許多可以直接拿來用的套件,這套理論與實作通稱為 OTP,是 Erlang 語言本身的一部份。
OTP 的概念是當 process 遵守一些共通的規範後,我們就可以用一致的方式啟動及關閉這些 OTP compliant process。
再進一步,我們可以做出一種專門管理其它 process 的 process,用來監視手下是否正常運作,並決定當事情不如預期時,應該如何重啟這些 process。這種 OTP process 叫做 Supervisor。
而因為 Supervisor 也是一顆 OTP process,所以 Supervisor 也可以管理 Supervisor。發展下去這些平行運算的 process 的每個環節,都有監控及從錯誤中回復的能力。而一個夠大的 Erlang / Elixir 應用程式,最後都會長成一顆 Supervision Tree。
是的。Phoenix 也是一顆 Supervision Tree。
要證明這點,我們在之前的 hello_phx
專案目錄裡,輸入這個指令:
$ iex -S mix phx.server
這個指令是先用 mix phx.server
跑起來後,再把 iex
掛載到運行中的節點上。如果是 Rails programmer 的話,你就想像是 rails s
及 rails c
的合體版。
在 iex 裡,輸入 :observer.start
iex> :observer.start
你會看到一個新的 GUI 介面,這是我們 hello_phx 系統的運作狀況:
切換到 [Applications],這就是這個專案裡每個 process 的從屬關係,也就是剛才所說的 Supervision Tree:
而介面裡的每個節點,都可以點進去看個別的詳細運作資訊。
要詳述 OTP 的內容,會寫出一本非常厚的書。Elixir,特別是 Phoenix 把這件事用很優雅的方式包裝過了,因此你可以在沒有意識到這現實的情況下開發好一陣子。但想要拿到 Erlang / Elixir 系統的全部好處,或遲或早你都會接觸到這一部份。
但是別擔心。在熟悉 Elixir 一陣子後,你會發現 Erlang 及 Elixir 的哲學及處理事情的方式非常接近,很容易就能讀得懂 Erlang 的書及程式碼,屆時再來深入探索也不遲。要記得的就是 Elixir 可以輕易的使用 OTP 的全部功能。
上一節說到除了 Erlang 內建的 OTP 之外,Elixir 也有一些自行包裝的函式庫。我們來看看 Task
這個用於平行計算的模組,它可以用 async/3
及 await/1
來進行平行運算。先新增一個檔案叫 tsk.exs
,並填入以下內容
# tsk.exs
defmodule Tsk do
def double(x) do
:timer.sleep(1000)
x * 2
end
end
然後打開 iex,先編譯這個模組,接著我們用 Task.async/3
調用它:
iex> c "tsk.exs"
[Tsk]
iex> task = Task.async(Tsk, :double, [100])
%Task{
owner: #PID<0.88.0>,
pid: #PID<0.96.0>,
ref: #Reference<0.3624132032.2388131842.168822>
}
接著用 Task.await/1
拿回傳的結果:
iex> Task.await(task)
200
在 Erlang 裡有個著名的函式叫 pmap/2
,就是我們之前提過的 Enum.map/2
,但是他會把每個元素的 map 計算,都新開一個 process 來處理。而用上 Task.async/3
及 Task.await/1
,我們也可以來做個一樣的東西。
defmodule Parallel do
def pmap(collection, func) do
collection
|> Enum.map(&(Task.async(fn -> func.(&1) end)))
|> Enum.map(&Task.await/1)
end
end
## iex
iex> result = Parallel.pmap 1..5, &(&1 * 2))
[2, 4, 6, 8, 10]
spawn/3
、send/2
、receive/1
來開新 process:observer.start
可以看系統運作的狀況Task
是 Elixir 包裝過的特化 OTPHappy hacking! 明天見。